/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.codec.http.multipart;

import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.http.HttpConstants;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.util.internal.StringUtil;

import java.nio.charset.Charset;
import java.util.List;

/**
 * This decoder will decode Body and can handle POST BODY.
 *
 * You <strong>MUST</strong> call {@link #destroy()} after completion to release
 * all resources.
 *
 */
public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder
{

    static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024;

    private final InterfaceHttpPostRequestDecoder decoder;

    /**
     *
     * @param request the request to decode
     * @throws NullPointerException for request
     * @throws ErrorDataDecoderException if the default charset was wrong when
     *         decoding or other errors
     */
    public HttpPostRequestDecoder(HttpRequest request)
    {
        this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE),
                request, HttpConstants.DEFAULT_CHARSET);
    }

    /**
     *
     * @param factory the factory used to create InterfaceHttpData
     * @param request the request to decode
     * @throws NullPointerException for request or factory
     * @throws ErrorDataDecoderException if the default charset was wrong when
     *         decoding or other errors
     */
    public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request)
    {
        this(factory, request, HttpConstants.DEFAULT_CHARSET);
    }

    /**
     *
     * @param factory the factory used to create InterfaceHttpData
     * @param request the request to decode
     * @param charset the charset to use as default
     * @throws NullPointerException for request or charset or factory
     * @throws ErrorDataDecoderException if the default charset was wrong when
     *         decoding or other errors
     */
    public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request,
            Charset charset)
    {
        if (factory == null)
        {
            throw new NullPointerException("factory");
        }
        if (request == null)
        {
            throw new NullPointerException("request");
        }
        if (charset == null)
        {
            throw new NullPointerException("charset");
        }
        // Fill default values
        if (isMultipart(request))
        {
            decoder = new HttpPostMultipartRequestDecoder(factory, request,
                    charset);
        }
        else
        {
            decoder = new HttpPostStandardRequestDecoder(factory, request,
                    charset);
        }
    }

    /**
     * states follow NOTSTARTED PREAMBLE ( (HEADERDELIMITER DISPOSITION (FIELD |
     * FILEUPLOAD))* (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE (MIXEDDELIMITER
     * MIXEDDISPOSITION MIXEDFILEUPLOAD)+ MIXEDCLOSEDELIMITER)* CLOSEDELIMITER)+
     * EPILOGUE
     *
     * First getStatus is: NOSTARTED
     *
     * Content-type: multipart/form-data, boundary=AaB03x => PREAMBLE in Header
     *
     * --AaB03x => HEADERDELIMITER content-disposition: form-data; name="field1"
     * => DISPOSITION
     *
     * Joe Blow => FIELD --AaB03x => HEADERDELIMITER content-disposition:
     * form-data; name="pics" => DISPOSITION Content-type: multipart/mixed,
     * boundary=BbC04y
     *
     * --BbC04y => MIXEDDELIMITER Content-disposition: attachment;
     * filename="file1.txt" => MIXEDDISPOSITION Content-Type: text/plain
     *
     * ... contents of file1.txt ... => MIXEDFILEUPLOAD --BbC04y =>
     * MIXEDDELIMITER Content-disposition: file; filename="file2.gif" =>
     * MIXEDDISPOSITION Content-type: image/gif Content-Transfer-Encoding:
     * binary
     *
     * ...contents of file2.gif... => MIXEDFILEUPLOAD --BbC04y-- =>
     * MIXEDCLOSEDELIMITER --AaB03x-- => CLOSEDELIMITER
     *
     * Once CLOSEDELIMITER is found, last getStatus is EPILOGUE
     */
    protected enum MultiPartStatus {
        NOTSTARTED, PREAMBLE, HEADERDELIMITER, DISPOSITION, FIELD, FILEUPLOAD, MIXEDPREAMBLE, MIXEDDELIMITER, MIXEDDISPOSITION, MIXEDFILEUPLOAD, MIXEDCLOSEDELIMITER, CLOSEDELIMITER, PREEPILOGUE, EPILOGUE
    }

    /**
     * Check if the given request is a multipart request
     * @return True if the request is a Multipart request
     */
    public static boolean isMultipart(HttpRequest request)
    {
        if (request.headers().contains(HttpHeaderNames.CONTENT_TYPE))
        {
            return getMultipartDataBoundary(request.headers()
                    .get(HttpHeaderNames.CONTENT_TYPE)) != null;
        }
        else
        {
            return false;
        }
    }

    /**
     * Check from the request ContentType if this request is a Multipart
     * request.
     * @return an array of String if multipartDataBoundary exists with the
     *         multipartDataBoundary as first element, charset if any as second
     *         (missing if not set), else null
     */
    protected static String[] getMultipartDataBoundary(String contentType)
    {
        // Check if Post using "multipart/form-data; boundary=--89421926422648
        // [; charset=xxx]"
        String[] headerContentType = splitHeaderContentType(contentType);
        if (headerContentType[0].toLowerCase()
                .startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString()))
        {
            int mrank;
            int crank;
            if (headerContentType[1].toLowerCase()
                    .startsWith(HttpHeaderValues.BOUNDARY.toString()))
            {
                mrank = 1;
                crank = 2;
            }
            else if (headerContentType[2].toLowerCase()
                    .startsWith(HttpHeaderValues.BOUNDARY.toString()))
            {
                mrank = 2;
                crank = 1;
            }
            else
            {
                return null;
            }
            String boundary = StringUtil
                    .substringAfter(headerContentType[mrank], '=');
            if (boundary == null)
            {
                throw new ErrorDataDecoderException("Needs a boundary value");
            }
            if (boundary.charAt(0) == '"')
            {
                String bound = boundary.trim();
                int index = bound.length() - 1;
                if (bound.charAt(index) == '"')
                {
                    boundary = bound.substring(1, index);
                }
            }
            if (headerContentType[crank].toLowerCase()
                    .startsWith(HttpHeaderValues.CHARSET.toString()))
            {
                String charset = StringUtil
                        .substringAfter(headerContentType[crank], '=');
                if (charset != null)
                {
                    return new String[]
                    { "--" + boundary, charset };
                }
            }
            return new String[]
            { "--" + boundary };
        }
        return null;
    }

    @Override
    public boolean isMultipart()
    {
        return decoder.isMultipart();
    }

    @Override
    public void setDiscardThreshold(int discardThreshold)
    {
        decoder.setDiscardThreshold(discardThreshold);
    }

    @Override
    public int getDiscardThreshold()
    {
        return decoder.getDiscardThreshold();
    }

    @Override
    public List<InterfaceHttpData> getBodyHttpDatas()
    {
        return decoder.getBodyHttpDatas();
    }

    @Override
    public List<InterfaceHttpData> getBodyHttpDatas(String name)
    {
        return decoder.getBodyHttpDatas(name);
    }

    @Override
    public InterfaceHttpData getBodyHttpData(String name)
    {
        return decoder.getBodyHttpData(name);
    }

    @Override
    public InterfaceHttpPostRequestDecoder offer(HttpContent content)
    {
        return decoder.offer(content);
    }

    @Override
    public boolean hasNext()
    {
        return decoder.hasNext();
    }

    @Override
    public InterfaceHttpData next()
    {
        return decoder.next();
    }

    @Override
    public InterfaceHttpData currentPartialHttpData()
    {
        return decoder.currentPartialHttpData();
    }

    @Override
    public void destroy()
    {
        decoder.destroy();
    }

    @Override
    public void cleanFiles()
    {
        decoder.cleanFiles();
    }

    @Override
    public void removeHttpDataFromClean(InterfaceHttpData data)
    {
        decoder.removeHttpDataFromClean(data);
    }

    /**
     * Split the very first line (Content-Type value) in 3 Strings
     *
     * @return the array of 3 Strings
     */
    private static String[] splitHeaderContentType(String sb)
    {
        int aStart;
        int aEnd;
        int bStart;
        int bEnd;
        int cStart;
        int cEnd;
        aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
        aEnd = sb.indexOf(';');
        if (aEnd == -1)
        {
            return new String[]
            { sb, "", "" };
        }
        bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1);
        if (sb.charAt(aEnd - 1) == ' ')
        {
            aEnd--;
        }
        bEnd = sb.indexOf(';', bStart);
        if (bEnd == -1)
        {
            bEnd = HttpPostBodyUtil.findEndOfString(sb);
            return new String[]
            { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), "" };
        }
        cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1);
        if (sb.charAt(bEnd - 1) == ' ')
        {
            bEnd--;
        }
        cEnd = HttpPostBodyUtil.findEndOfString(sb);
        return new String[]
        { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd),
                sb.substring(cStart, cEnd) };
    }

    /**
     * Exception when try reading data from request in chunked format, and not
     * enough data are available (need more chunks)
     */
    public static class NotEnoughDataDecoderException extends DecoderException
    {
        private static final long serialVersionUID = -7846841864603865638L;

        public NotEnoughDataDecoderException()
        {
        }

        public NotEnoughDataDecoderException(String msg)
        {
            super(msg);
        }

        public NotEnoughDataDecoderException(Throwable cause)
        {
            super(cause);
        }

        public NotEnoughDataDecoderException(String msg, Throwable cause)
        {
            super(msg, cause);
        }
    }

    /**
     * Exception when the body is fully decoded, even if there is still data
     */
    public static class EndOfDataDecoderException extends DecoderException
    {
        private static final long serialVersionUID = 1336267941020800769L;
    }

    /**
     * Exception when an error occurs while decoding
     */
    public static class ErrorDataDecoderException extends DecoderException
    {
        private static final long serialVersionUID = 5020247425493164465L;

        public ErrorDataDecoderException()
        {
        }

        public ErrorDataDecoderException(String msg)
        {
            super(msg);
        }

        public ErrorDataDecoderException(Throwable cause)
        {
            super(cause);
        }

        public ErrorDataDecoderException(String msg, Throwable cause)
        {
            super(msg, cause);
        }
    }
}
